import Node from "./node";
import { getNodeKey } from "./util";

export default class TreeStore {
    constructor(options) {
        this.currentNode = null;
        this.currentNodeKey = null;

        for (let option in options) {
            // eslint-disable-next-line no-prototype-builtins
            if (options.hasOwnProperty(option)) {
                this[option] = options[option];
            }
        }

        this.nodesMap = {};

        this.root = new Node({
            data: this.data,
            store: this,
        });

        if (this.lazy && this.load) {
            const loadFn = this.load;
            loadFn(this.root, (data) => {
                this.root.doCreateChildren(data);
                this._initDefaultCheckedNodes();
            });
        } else {
            this._initDefaultCheckedNodes();
        }
    }

    filter(value) {
        const filterNodeMethod = this.filterNodeMethod;
        const lazy = this.lazy;
        const traverse = function(node) {
            const childNodes = node.root ?
                node.root.childNodes :
                node.childNodes;

            childNodes.forEach((child) => {
                child.visible = filterNodeMethod.call(
                    child,
                    value,
                    child.data,
                    child,
                );

                traverse(child);
            });

            if (!node.visible && childNodes.length) {
                let allHidden = true;
                allHidden = !childNodes.some((child) => child.visible);

                if (node.root) {
                    node.root.visible = allHidden === false;
                } else {
                    node.visible = allHidden === false;
                }
            }
            if (!value) return;

            if (node.visible && !node.isLeaf && !lazy) node.expand();
        };

        traverse(this);
    }

    setData(newVal) {
        const instanceChanged = newVal !== this.root.data;
        if (instanceChanged) {
            this.root.setData(newVal);
            this._initDefaultCheckedNodes();
        } else {
            this.root.updateChildren();
        }
    }

    getNode(data) {
        if (data instanceof Node) return data;
        const key =
            typeof data !== "object" ? data : getNodeKey(this.key, data);
        return this.nodesMap[key] || null;
    }

    insertBefore(data, refData) {
        const refNode = this.getNode(refData);
        refNode.parent.insertBefore({ data }, refNode);
    }

    insertAfter(data, refData) {
        const refNode = this.getNode(refData);
        refNode.parent.insertAfter({ data }, refNode);
    }

    remove(data) {
        const node = this.getNode(data);

        if (node && node.parent) {
            if (node === this.currentNode) {
                this.currentNode = null;
            }
            node.parent.removeChild(node);
        }
    }

    append(data, parentData) {
        const parentNode = parentData ? this.getNode(parentData) : this.root;

        if (parentNode) {
            parentNode.insertChild({ data });
        }
    }

    _initDefaultCheckedNodes() {
        const defaultCheckedKeys = this.defaultCheckedKeys || [];
        const nodesMap = this.nodesMap;

        defaultCheckedKeys.forEach((checkedKey) => {
            const node = nodesMap[checkedKey];

            if (node) {
                node.setChecked(true, !this.checkStrictly);
            }
        });
    }

    _initDefaultCheckedNode(node) {
        const defaultCheckedKeys = this.defaultCheckedKeys || [];

        if (defaultCheckedKeys.indexOf(node.key) !== -1) {
            node.setChecked(true, !this.checkStrictly);
        }
    }

    setDefaultCheckedKey(newVal) {
        if (newVal !== this.defaultCheckedKeys) {
            this.defaultCheckedKeys = newVal;
            this._initDefaultCheckedNodes();
        }
    }

    registerNode(node) {
        const key = this.key;
        if (!key || !node || !node.data) return;

        const nodeKey = node.key;
        if (nodeKey !== undefined) this.nodesMap[node.key] = node;
    }

    deregisterNode(node) {
        const key = this.key;
        if (!key || !node || !node.data) return;

        node.childNodes.forEach((child) => {
            this.deregisterNode(child);
        });

        delete this.nodesMap[node.key];
    }

    getCheckedNodes(leafOnly = false, includeHalfChecked = false) {
        const checkedNodes = [];
        const traverse = function(node) {
            const childNodes = node.root ?
                node.root.childNodes :
                node.childNodes;

            childNodes.forEach((child) => {
                if (
                    (child.checked ||
                        (includeHalfChecked && child.indeterminate)) &&
                    (!leafOnly || (leafOnly && child.isLeaf))
                ) {
                    checkedNodes.push(child.data);
                }

                traverse(child);
            });
        };

        traverse(this);

        return checkedNodes;
    }

    getCheckedKeys(leafOnly = false) {
        return this.getCheckedNodes(leafOnly).map(
            (data) => (data || {})[this.key],
        );
    }

    getHalfCheckedNodes() {
        const nodes = [];
        const traverse = function(node) {
            const childNodes = node.root ?
                node.root.childNodes :
                node.childNodes;

            childNodes.forEach((child) => {
                if (child.indeterminate) {
                    nodes.push(child.data);
                }

                traverse(child);
            });
        };

        traverse(this);

        return nodes;
    }

    getHalfCheckedKeys() {
        return this.getHalfCheckedNodes().map((data) => (data || {})[this.key]);
    }

    _getAllNodes() {
        const allNodes = [];
        const nodesMap = this.nodesMap;
        for (let nodeKey in nodesMap) {
            // eslint-disable-next-line no-prototype-builtins
            if (nodesMap.hasOwnProperty(nodeKey)) {
                allNodes.push(nodesMap[nodeKey]);
            }
        }

        return allNodes;
    }

    updateChildren(key, data) {
        const node = this.nodesMap[key];
        if (!node) return;
        const childNodes = node.childNodes;
        for (let i = childNodes.length - 1; i >= 0; i--) {
            const child = childNodes[i];
            this.remove(child.data);
        }
        for (let i = 0, j = data.length; i < j; i++) {
            const child = data[i];
            this.append(child, node.data);
        }
    }

    _setCheckedKeys(key, leafOnly = false, checkedKeys) {
        const allNodes = this._getAllNodes().sort((a, b) => b.level - a.level);
        const cache = Object.create(null);
        const keys = Object.keys(checkedKeys);
        allNodes.forEach((node) => node.setChecked(false, false));
        for (let i = 0, j = allNodes.length; i < j; i++) {
            const node = allNodes[i];
            const nodeKey = node.data[key].toString();
            let checked = keys.indexOf(nodeKey) > -1;
            if (!checked) {
                if (node.checked && !cache[nodeKey]) {
                    node.setChecked(false, false);
                }
                continue;
            }

            let parent = node.parent;
            while (parent && parent.level > 0) {
                cache[parent.data[key]] = true;
                parent = parent.parent;
            }

            if (node.isLeaf || this.checkStrictly) {
                node.setChecked(true, false);
                continue;
            }
            node.setChecked(true, true);

            if (leafOnly) {
                node.setChecked(false, false);
                const traverse = function(node) {
                    const childNodes = node.childNodes;
                    childNodes.forEach((child) => {
                        if (!child.isLeaf) {
                            child.setChecked(false, false);
                        }
                        traverse(child);
                    });
                };
                traverse(node);
            }
        }
    }

    setCheckedNodes(array, leafOnly = false) {
        const key = this.key;
        const checkedKeys = {};
        array.forEach((item) => {
            checkedKeys[(item || {})[key]] = true;
        });

        this._setCheckedKeys(key, leafOnly, checkedKeys);
    }

    setCheckedKeys(keys, leafOnly = false) {
        this.defaultCheckedKeys = keys;
        const key = this.key;
        const checkedKeys = {};
        keys.forEach((key) => {
            checkedKeys[key] = true;
        });

        this._setCheckedKeys(key, leafOnly, checkedKeys);
    }

    setDefaultExpandedKeys(keys) {
        keys = keys || [];
        this.defaultExpandedKeys = keys;

        keys.forEach((key) => {
            const node = this.getNode(key);
            if (node) node.expand(null, this.autoExpandParent);
        });
    }

    setChecked(data, checked, deep) {
        const node = this.getNode(data);

        if (node) {
            node.setChecked(!!checked, deep);
        }
    }

    getCurrentNode() {
        return this.currentNode;
    }

    setCurrentNode(currentNode) {
        const prevCurrentNode = this.currentNode;
        if (prevCurrentNode) {
            prevCurrentNode.isCurrent = false;
        }
        this.currentNode = currentNode;
        this.currentNode.isCurrent = true;
    }

    setUserCurrentNode(node) {
        const key = node[this.key];
        const currNode = this.nodesMap[key];
        this.setCurrentNode(currNode);
    }

    setCurrentNodeKey(key) {
        if (key === null || key === undefined) {
            this.currentNode && (this.currentNode.isCurrent = false);
            this.currentNode = null;
            return;
        }
        const node = this.getNode(key);
        if (node) {
            this.setCurrentNode(node);
        }
    }
}